<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>3D 交互式星系生成器</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      body {
        overflow: hidden;
        background-color: #000;
      }
      canvas {
        display: block;
      }
      #instructions {
        position: absolute;
        top: 20px;
        left: 20px;
        color: rgba(255, 255, 255, 0.8);
        font-family: "Arial", sans-serif;
        pointer-events: none;
        background: rgba(0, 0, 0, 0.5);
        padding: 15px;
        border-radius: 8px;
        border: 1px solid rgba(255, 255, 255, 0.2);
      }
      h1 {
        font-size: 18px;
        margin-bottom: 5px;
        text-transform: uppercase;
        letter-spacing: 2px;
        color: #00ffff;
      }
      p {
        font-size: 12px;
        line-height: 1.5;
      }
    </style>
  </head>
  <body>
    <div id="instructions">
      <h1>星系造物主</h1>
      <p>左键拖拽：旋转视角</p>
      <p>滚轮滑动：缩放宇宙</p>
      <p>右上角面板：调整参数并重造星系</p>
    </div>

    <!-- 引入 Three.js 和相关库 -->
    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
          "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
        }
      }
    </script>

    <script type="module">
      import * as THREE from "three";
      import { OrbitControls } from "three/addons/controls/OrbitControls.js";
      import { GUI } from "three/addons/libs/lil-gui.module.min.js";

      /**
       * 基础设置
       */
      // 场景
      const scene = new THREE.Scene();

      // 渲染器
      const renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      document.body.appendChild(renderer.domElement);

      // 相机
      const camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.x = 3;
      camera.position.y = 3;
      camera.position.z = 5;

      // 控制器 (鼠标交互)
      const controls = new OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      controls.dampingFactor = 0.05;

      /**
       * 星系生成逻辑
       */
      const parameters = {
        count: 50000, // 粒子数量
        size: 0.015, // 粒子大小
        radius: 5, // 星系半径
        branches: 3, // 旋臂数量
        spin: 1, // 弯曲程度
        randomness: 0.2, // 随机扩散度
        randomnessPower: 3, // 聚集程度
        insideColor: "#ff6030", // 核心颜色
        outsideColor: "#1b3984", // 边缘颜色
        rotationSpeed: 0.001, // 自转速度
      };

      let geometry = null;
      let material = null;
      let points = null;

      const generateGalaxy = () => {
        // 清除旧的星系
        if (points !== null) {
          geometry.dispose();
          material.dispose();
          scene.remove(points);
        }

        /**
         * 几何体数据
         */
        geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(parameters.count * 3);
        const colors = new Float32Array(parameters.count * 3);

        const colorInside = new THREE.Color(parameters.insideColor);
        const colorOutside = new THREE.Color(parameters.outsideColor);

        for (let i = 0; i < parameters.count; i++) {
          const i3 = i * 3;

          // 半径位置
          const radius = Math.random() * parameters.radius;

          // 旋臂角度
          const spinAngle = radius * parameters.spin;
          const branchAngle =
            ((i % parameters.branches) / parameters.branches) * Math.PI * 2;

          // 随机偏移 (用于制造云雾感)
          const randomX =
            Math.pow(Math.random(), parameters.randomnessPower) *
            (Math.random() < 0.5 ? 1 : -1) *
            parameters.randomness *
            radius;
          const randomY =
            Math.pow(Math.random(), parameters.randomnessPower) *
            (Math.random() < 0.5 ? 1 : -1) *
            parameters.randomness *
            radius;
          const randomZ =
            Math.pow(Math.random(), parameters.randomnessPower) *
            (Math.random() < 0.5 ? 1 : -1) *
            parameters.randomness *
            radius;

          positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
          positions[i3 + 1] = randomY; // Y轴扁平
          positions[i3 + 2] =
            Math.sin(branchAngle + spinAngle) * radius + randomZ;

          // 颜色混合
          const mixedColor = colorInside.clone();
          mixedColor.lerp(colorOutside, radius / parameters.radius);

          colors[i3] = mixedColor.r;
          colors[i3 + 1] = mixedColor.g;
          colors[i3 + 2] = mixedColor.b;
        }

        geometry.setAttribute(
          "position",
          new THREE.BufferAttribute(positions, 3)
        );
        geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

        /**
         * 材质
         */
        material = new THREE.PointsMaterial({
          size: parameters.size,
          sizeAttenuation: true,
          depthWrite: false,
          blending: THREE.AdditiveBlending, // 叠加混合模式，产生发光效果
          vertexColors: true,
        });

        /**
         * 创建点云
         */
        points = new THREE.Points(geometry, material);
        scene.add(points);
      };

      generateGalaxy();

      /**
       * GUI 控制面板
       */
      const gui = new GUI({ width: 300, title: "星系参数控制台" });

      gui
        .add(parameters, "count")
        .min(1000)
        .max(200000)
        .step(100)
        .name("星星数量")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "size")
        .min(0.001)
        .max(0.1)
        .step(0.001)
        .name("星星大小")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "radius")
        .min(0.1)
        .max(20)
        .step(0.1)
        .name("星系半径")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "branches")
        .min(2)
        .max(20)
        .step(1)
        .name("旋臂数量")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "spin")
        .min(-5)
        .max(5)
        .step(0.01)
        .name("旋臂弯曲")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "randomness")
        .min(0)
        .max(2)
        .step(0.01)
        .name("星云扩散")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "randomnessPower")
        .min(1)
        .max(10)
        .step(0.01)
        .name("核心聚集度")
        .onFinishChange(generateGalaxy);
      gui
        .addColor(parameters, "insideColor")
        .name("核心颜色")
        .onFinishChange(generateGalaxy);
      gui
        .addColor(parameters, "outsideColor")
        .name("边缘颜色")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "rotationSpeed")
        .min(0)
        .max(0.02)
        .step(0.0001)
        .name("自转速度");

      /**
       * 背景星空
       */
      const bgGeometry = new THREE.BufferGeometry();
      const bgCount = 2000;
      const bgPositions = new Float32Array(bgCount * 3);
      for (let i = 0; i < bgCount * 3; i++) {
        bgPositions[i] = (Math.random() - 0.5) * 50; // 远处背景
      }
      bgGeometry.setAttribute(
        "position",
        new THREE.BufferAttribute(bgPositions, 3)
      );
      const bgMaterial = new THREE.PointsMaterial({
        size: 0.05,
        color: 0x444444,
      });
      const bgStars = new THREE.Points(bgGeometry, bgMaterial);
      scene.add(bgStars);

      /**
       * 动画循环
       */
      const clock = new THREE.Clock();

      const tick = () => {
        const elapsedTime = clock.getElapsedTime();

        // 旋转动画
        if (points) {
          points.rotation.y = elapsedTime * parameters.rotationSpeed * -10;
          // 让背景也微微移动，增加立体感
          bgStars.rotation.y = elapsedTime * 0.02;
        }

        // 更新控制器
        controls.update();

        // 渲染
        renderer.render(scene, camera);

        // 下一帧
        window.requestAnimationFrame(tick);
      };

      tick();

      /**
       * 窗口大小调整
       */
      window.addEventListener("resize", () => {
        // 更新相机
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        // 更新渲染器
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      });
    </script>
  </body>
</html>
